/****************************************************************************
* Copyright (c) 2008, j. montgomery, ATGi                                   *
* All rights reserved.                                                      *
*                                                                           *
* Redistribution and use in source and binary forms, with or without        *
* modification, are permitted provided that the following conditions        *
* are met:                                                                  *
*                                                                           *
* - Redistributions of source code must retain the above copyright          *
*   notice, this list of conditions and the following disclaimer.           *
*                                                                           *
* - Redistributions in binary form must reproduce the above                 *
*   copyright notice, this list of conditions and the following             *
*   disclaimer in the documentation and/or other materials provided         *
*   with the distribution.                                                  *
*                                                                           *
* - Neither the name of ATGi nor the names of its contributors may          *
*   be used to endorse or promote products derived from this software       *
*   without specific prior written permission.                              *
*                                                                           *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS       *
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT         *
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS         *
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE            *
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,       *
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES                  *
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR        *
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)        *
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,       *
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)             *
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED       *
* OF THE POSSIBILITY OF SUCH DAMAGE.                                        *
*****************************************************************************
*/
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization; 
using System.IO;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using DnSmtp.Message;

using DnDns.Query;
using DnDns.Enums;
using DnDns.Records;

namespace DnSmtp
{
    /// <summary>
    /// 
    /// </summary>
    public enum DeliveryMethod
    {
        /// <summary>
        /// 
        /// </summary>
        DirectDelivery,
        /// <summary>
        /// 
        /// </summary>
        Relay
    }

	internal delegate void SendmailWithToAddressCallback(MailMessage message, EmailAddress toAddress);
	internal delegate void SendmailCallback(MailMessage message);

	/// <summary>
	/// Primary functional class for sending SMTP mail messages.
	/// </summary>
	/// <remarks>Requires at a minimum to set an SmtpServer value before calling Send.</remarks>
	/// <example language="c#">
	/// <code>
	/// Mail mailObj = new Mail();
	/// mailObj.SmtpServer = "192.168.1.100";
	/// MailMessage message = new MailMessage(messageTo,messageFrom,messageSubject);
	/// message.AddTextMessage(String.Format(messageBodyFormat, greeting,body,closing));
	/// 
	/// AsyncCallback callback = delegate(IAsyncResult result)
	/// {
	/// 	   mailObj.EndSend(result);	 
	///     //do something when complete.
	/// };
	///
	/// object asyncState = null;
	/// mailObj.BeginSend(message, callback, asyncState);
	/// //do other stuff.
	/// </code>
	/// </example>
	public class Mail
	{
		#region Fields
		private SendmailCallback _sendmailCallback;
		private SendmailWithToAddressCallback _sendmailWithToAddrCallback;
		private SmtpHandler _smtpHandler;
		private RecipientFailedException _rfe;
		private StringDictionary _dnsCache = new StringDictionary();
		private TraceLevel traceLevel = TraceLevel.Error;
		#endregion Fields

		#region Properties
		/// <summary>
		/// The SmtpLog contains a copy of the converstaion between the SmtpHandler and the SMTP Server.
		/// </summary>
		public string SmtpLog 
		{
			get { return _smtpHandler.SmtpLog.ToString(); }
		}

		/// <summary>
		/// The IP address to show up in the SMTP HELO - "HELO aaa.bbb.ccc.ddd" 
		/// </summary>
		public string LocalHostIp
		{
			get
			{
				if (_hostIp == null)
				{
					NetworkInterface[] interfaces  = NetworkInterface.GetAllNetworkInterfaces();
					foreach(NetworkInterface netinterface in interfaces)
					{
						if (netinterface.NetworkInterfaceType != NetworkInterfaceType.Loopback)
						{
							UnicastIPAddressInformationCollection addresses = netinterface.GetIPProperties().UnicastAddresses;
							if (addresses.Count > 0)
							{
								_hostIp = addresses[0].Address;
								break;
							}
						}
					}
				}
				if(_hostIp != null)
					return _hostIp.ToString();
				return String.Empty;
			}
			set
			{
				_hostIp = IPAddress.Parse(value);
			}
		} private IPAddress _hostIp;

		/// <summary>
		/// The SmtpServer contains an IP address or dns name of the SMTP server.
		/// </summary>
		public string SmtpServer 
		{
			get { return _smtpServer; }
			set { _smtpServer = value; }
		} private string _smtpServer;
		
		private string _dnsServerIP;
		/// <summary>
		/// Used to override the Detected IP of a DNS server - this server is used to lookup MX records to 
        /// connect directly to the mail servers.
		/// </summary>
		public string DnsServerIPAddress
		{
			get
			{
				return _dnsServerIP;
			}
			set
			{
				_dnsServerIP = value;
			}
		}


		/// <summary>
		/// Connection Timeout (in seconds)
		/// </summary>
		public int ConnectionTimeout 
		{
			get { return _smtpHandler.ConnectionTimeout; }
			set { _smtpHandler.ConnectionTimeout = value; }
		}
		#endregion Properties

		#region Constructors
		/// <summary>
        /// </summary>
        /// <param name="serverAddress">The IP Address or DNS Server or the SMTP Server, depending on the DeliveryMethod</param>
        /// <param name="method">If DirectDelivery, DNS will be used to resolve the remote Mail Server and connect directly to it and deliver it.
        /// If Relay, all mail messages will be sent through the local mail server specified by the serverAddress.</param>

        public Mail(string serverAddress, DeliveryMethod method)
		{
            switch (method)
            {
                case DeliveryMethod.DirectDelivery:
                    _dnsServerIP = serverAddress;

                    if (_dnsServerIP == null)
                        this.DiscoverDnsServer();
                    break;
                case DeliveryMethod.Relay:
                    _smtpServer = serverAddress;
                    break;
            }

			_smtpHandler = new SmtpHandler();
			_sendmailWithToAddrCallback = new SendmailWithToAddressCallback(this.Send);
			_sendmailCallback = new SendmailCallback(this.Send);
			
			_rfe = new RecipientFailedException();
            
		}

		/// <summary>
        /// Initializes a new instance of the SmtpHandler with defaults.
        /// Will use DirectDelivery method for each recipient and will detect the local DNS Server
        /// </summary>
		public Mail() : this(null, DeliveryMethod.DirectDelivery) 
		{
		}
		#endregion Constructors

        private void DiscoverDnsServer()
        {
            NetworkInterface[] ifs = NetworkInterface.GetAllNetworkInterfaces();
            foreach (NetworkInterface netInterface in ifs)
            {
                // Find the first Interface that is UP that isn't the loopback.
                if ((netInterface.OperationalStatus == OperationalStatus.Up) && (netInterface.NetworkInterfaceType != NetworkInterfaceType.Loopback))
                {
                    IPInterfaceProperties ip = netInterface.GetIPProperties();
                    if (ip.DnsAddresses.Count > 0)
                    {
                        if (ip.DnsAddresses[0].AddressFamily == AddressFamily.InterNetwork)
                        {
                            _dnsServerIP = ip.DnsAddresses[0].ToString();
                        }
                        else
                        {
                            throw new InvalidOperationException("Unsupported Address Family: " + ip.DnsAddresses[0].AddressFamily.ToString());
                        }
                    }
                }
            }
        }

		public void Dispose()
		{
			this._rfe = null;
		}

		/// <summary>
		/// Begins an asynchronous operation to send a mail message using SMTP.
		/// </summary>
		/// <param name="message"></param>
		/// <param name="callback"></param>
		/// <param name="asyncState"></param>
		/// <returns></returns>
		public System.IAsyncResult BeginSend(MailMessage message, System.AsyncCallback callback, object asyncState) 
		{
			return _sendmailCallback.BeginInvoke(message, callback, asyncState);
		}

		/// <summary>
		/// Begins an asynchronous operation to send a mail message using SMTP.
		/// </summary>
		/// <param name="message"></param>
		/// <param name="toAddress"></param>
		/// <param name="callback"></param>
		/// <param name="asyncState"></param>
		/// <returns></returns>
		public System.IAsyncResult BeginSendWithToAddress(MailMessage message, EmailAddress toAddress, System.AsyncCallback callback, object asyncState) 
		{
			return _sendmailWithToAddrCallback.BeginInvoke(message, toAddress, callback, asyncState);
		}

		/// <summary>
		/// Completes asynchronus invocation of BeginSendWithToAddress
		/// </summary>
		/// <param name="ar">Result of begin op</param>
		public void EndSendWithToAddress(IAsyncResult ar)
		{
			_sendmailWithToAddrCallback.EndInvoke(ar);
		}

		/// <summary>
		/// Completes asynchronous invocation of mail sending operation using SMTP.
		/// </summary>
		/// <param name="ar"></param>
		public void EndSend(IAsyncResult ar) 
		{
			_sendmailCallback.EndInvoke(ar);
		}

		/// <summary>
		/// Sends the message to each recipient in the Message.ToAddress and CcAddress, and BccAddress array lists.
		/// </summary>
		/// <param name="message">The message to be sent</param>
		public void Send(MailMessage message)
		{
			while(message.SynchRecipientStack.Count > 0)
			{
				this.Send(message, (EmailAddress)message.SynchRecipientStack.Pop());
			}
		}

		public void Send(MailMessage message, string toAddress)
		{
			Send(message, new EmailAddress(toAddress));
		}

		/// <summary>
		/// The SendMail method contains the basic logic for delivering a Message and throwing a 
		/// RecipientFailedException if necessary. This is also the method that records the Smtp 
		/// Converstaion in the SmtpMail Event Log.
		/// </summary>
		/// <remarks>This method ignores any addresses in the Message object's TO,CC, and BCC fields. To
		/// send a message to multiple recipients use the <see cref="Send(MailMessage)"/> overload.
		/// </remarks>
		/// <param name="message">The message to send to the given recipient.</param>
		/// <param name="toAddress">A string that contains an email address.</param>
		public void Send(MailMessage message, EmailAddress toAddress) 
		{
			if (String.IsNullOrEmpty(_smtpServer) && String.IsNullOrEmpty(_dnsServerIP))
			{
				throw new InvalidOperationException("You must either specify the smtp server through which to send mail, or a DNS server IP from which to obtain MX records before sending any mail.");
			}
			try
			{
				string smtpServer = (String.IsNullOrEmpty(_smtpServer)) ? GetSmtpServerForAddress(toAddress) : _smtpServer;
				_smtpHandler.Connect(smtpServer, IPAddress.Parse(LocalHostIp));

                try 
                {
					this.SendMailMessage(message, toAddress.Email);
                } 
				finally 
				{
					_smtpHandler.Disconnect();
					Trace.WriteLineIf(_smtpHandler.TraceLevel  == TraceLevel.Verbose, SmtpLog, EventLogEntryType.Information.ToString());
				}
			}
			catch(System.Net.Sockets.SocketException se)
			{
                _rfe.AddFailedRecipient(new FailedRecipient(se, toAddress));
				Trace.WriteLineIf(_smtpHandler.TraceLevel  >= TraceLevel.Error, " Failed Recipient " + toAddress.Email, "SendMail");
                Trace.WriteLineIf(_smtpHandler.TraceLevel >= TraceLevel.Error, _rfe.FailedRecipients.Count.ToString(), "SendMail");
			}
			catch(Exception e) 
			{
				_rfe.AddFailedRecipient(new FailedRecipient(e, toAddress));
				Trace.WriteLineIf(_smtpHandler.TraceLevel  >= TraceLevel.Error, " Failed Recipient " + toAddress.Email, "SendMail");
				Trace.WriteLineIf(_smtpHandler.TraceLevel  >= TraceLevel.Error, _rfe.FailedRecipients.Count.ToString(), "SendMail");
			}
				
			if(_rfe.FailedRecipients.Count > 0)
			{
				// Throw a RecipientFailedException if the any of the recipients failed.
				throw _rfe;
			}
		}

		// This Method tells the SMTP server who the Message is TO, FROM, 
		// builds the Mail Headers, Message Body and commits it for delivery.
		private bool SendMailMessage(MailMessage message, string toAddr) 
		{
			DateTime dtNow = DateTime.Now;
			string failedEmailList = "";
			StringBuilder sb = new StringBuilder();

			_smtpHandler.SendCommand("MAIL FROM: <" + message.FromAddress.Email + ">");
			_smtpHandler.SendCommand("RCPT TO: <" + toAddr + ">");
			failedEmailList = toAddr;
			_smtpHandler.SendCommand("DATA");

			sb.Append(message.BuildMessage());
#if LOG
 			System.IO.StreamWriter sw = System.IO.File.AppendText(@"c:\inetpub\wwwroot\smtp.log");
			sw.Write(sb.ToString() + 
				"********************" + 
				Environment.NewLine + 
				"******************" +
				Environment.NewLine +
				this.SmtpLog);
			sw.Flush();
			sw.Close();
#endif
			sb.Append(MailMessage.CRLF);
			sb.Append(".");
			sb.Append(MailMessage.CRLF);
			_smtpHandler.SendCommand(sb.ToString());
			return true;
		}

		private string GetSmtpServerForAddress(EmailAddress address)
		{
			string sSmtpServerHostAddr = "";		
			try
			{
				Trace.WriteLineIf(traceLevel == TraceLevel.Verbose, " Resolving DNS...", Environment.TickCount + " : " + Thread.CurrentThread.Name + " : SendMailThreads");
				if (_dnsCache.ContainsKey(address.Domain.ToLower()))
				{
					sSmtpServerHostAddr = _dnsCache[address.Domain];
				}
				else
				{
					Trace.WriteLineIf(traceLevel == TraceLevel.Verbose, " Spawning Dns Thread", Environment.TickCount + " : " + Thread.CurrentThread.Name + " : SendMailThreads");
					//Thread t = SimpleThread.CreateThread(new SimpleThread.Start(this.GetSmtpSvrAddr), _dnsServerIP, address.Domain);
					//t.Name = "DnsQueryRequest";
					//t.Start();
					//if (t.Join(10000))
					//{
						Trace.WriteLineIf(traceLevel == TraceLevel.Verbose, "Reading Dns Cache", Environment.TickCount + " : " + Thread.CurrentThread.Name + " : SendMailThreads");
						this.GetSmtpServerAddress(_dnsServerIP, address.Domain);
						sSmtpServerHostAddr = _dnsCache[address.Domain];
					//    if (sSmtpServerHostAddr.Length <= 0)
					//    {
					//        throw new RecipientFailedException("Dns Lookup Failure");
					//    }
					//}
					//else
					//{
					//    t.Abort();
					//    Trace.WriteLineIf(traceLevel == TraceLevel.Verbose, " Failed Adding to Dns Cache", Environment.TickCount + " : " + Thread.CurrentThread.Name + " : SendMailThreads");
					//    throw new RecipientFailedException("Dns Timeout");
					//}
				}
			}
			catch (ThreadAbortException)
			{
				//Not done executing, just want to avoid throwing an RFE since it wasn't a failure - it was canceled
				Trace.WriteLineIf(traceLevel >= TraceLevel.Error, " Message Aborted to : " + address.Email, Environment.TickCount + " : " + Thread.CurrentThread.Name + " : SendMailThreads");
			}
			catch (Exception e)
			{
				Trace.WriteLineIf(traceLevel >= TraceLevel.Error, " Failed DNS Lookup: " + e.Message, Environment.TickCount + " : " + Thread.CurrentThread.Name + " : SendMailThreads");
				throw new ApplicationException("Failed Dns Lookup", e);
			}
			return sSmtpServerHostAddr;
		}

		private void GetSmtpServerAddress(string dnsServerIp, string domainName)
		{
			Trace.WriteLineIf(traceLevel <= TraceLevel.Verbose, " Starting Dns Lookup for " + dnsServerIp + " with domain name " + domainName, Environment.TickCount + " : " + Thread.CurrentThread.Name + " : GetSmtpSvrAddr");

			string sMxServerName = String.Empty;
			string sMxServerHostAddr = String.Empty;

			DnsQueryRequest dnsQ = new DnsQueryRequest();
			DnsQueryResponse dnsQR = dnsQ.Resolve(dnsServerIp, domainName, NsType.MX, NsClass.INET, ProtocolType.Udp);

			Trace.WriteLineIf(traceLevel == TraceLevel.Verbose, " Reading Query Response", Environment.TickCount + " : " + Thread.CurrentThread.Name + " : GetSmtpSvrAddr");
			SortedList sl = new SortedList();
			for (int i = 0; i < dnsQR.Answers.Length; i++)
			{
				MxRecord mxRec = (MxRecord)dnsQR.Answers[i];
				if (!sl.Contains(mxRec.Preference))
				{
					sl.Add(mxRec.Preference, mxRec.MailExchange);
				}
			}

			try
			{
				sMxServerName = (string)sl.GetByIndex(0);
			}
			catch (ArgumentOutOfRangeException)
			{
				sMxServerName = "";
			}

			Trace.WriteLineIf(traceLevel == TraceLevel.Verbose, " Adding Response to Cache: " + sMxServerName, Environment.TickCount + " : " + Thread.CurrentThread.Name + " : GetSmtpSvrAddr");
			if (sMxServerName.Length != 0)
			{
				for (int i = 0; i < dnsQR.AdditionalRecords.Length; i++)
				{
					ARecord aRec = (ARecord)dnsQR.AdditionalRecords[i];
					if (sMxServerName.Equals(aRec.DnsHeader.Name))
					{
						sMxServerHostAddr = aRec.HostAddress;
					}
				}

				if (sMxServerHostAddr.Trim().Length == 0)
				{
                    DnsQueryResponse dnsQR2 = dnsQ.Resolve(dnsServerIp, sMxServerName, NsType.A, NsClass.INET, ProtocolType.Udp);

					for (int i = 0; i < dnsQR2.Answers.Length; i++)
					{
						ARecord aRec = (ARecord)dnsQR2.Answers[i];
						sMxServerHostAddr = aRec.HostAddress;
					}
				}
			}

			lock (_dnsCache.SyncRoot)
			{
				if (!_dnsCache.ContainsKey(domainName.ToLower()))
					_dnsCache.Add(domainName.ToLower(), sMxServerHostAddr);
				return;
			}

		}
	}
}
